package com.demo.service;

import com.demo.constant.DataSourceInfo;
import com.demo.mapper.UserItemMapper;
import com.demo.model.UserItem;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.eval.IRStatistics;
import org.apache.mahout.cf.taste.eval.RecommenderBuilder;
import org.apache.mahout.cf.taste.eval.RecommenderEvaluator;
import org.apache.mahout.cf.taste.eval.RecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator;
import org.apache.mahout.cf.taste.impl.eval.AverageAbsoluteDifferenceRecommenderEvaluator;
import org.apache.mahout.cf.taste.impl.eval.GenericRecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.impl.model.jdbc.MySQLJDBCDataModel;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericItemBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
import org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.JDBCDataModel;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.Recommender;
import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;
import org.apache.mahout.common.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author: LiuYuanQi
 * @Date: 2020/4/15 10:20
 */

@Service
public class RecommendService {

    @Autowired
    private UserItemMapper userItemMapper;

    public List forUser() throws TasteException, IOException {
        int NEIGHBORHOOD_NUM = 10;   //用户邻居数量
        int RECOMMENDER_NUM = 30;    //推荐结果个数

//        JDBCDataModel dataModel = getJDBCDataModel();
        ClassPathResource classPathResource = new ClassPathResource("movie.data");
        DataModel dataModel  = new FileDataModel(classPathResource.getFile());

        /*
        计算相似度，相似度算法有很多种，欧几里得、皮尔逊等等。
        基于用户的协同过滤算法，基于物品的协同过滤算法，这里使用了EuclideanDistanceSimilarity
        计算欧式距离，欧式距离来定义相似性，用s=1/(1+d)来表示，范围在[0,1]之间，值越大，表明d越小，距离越近，则表示相似性越大
        */
        UserSimilarity similarity = new EuclideanDistanceSimilarity(dataModel);
        /*
        计算最近邻域，邻居有两种算法，基于固定数量的邻居和基于相似度的邻居，这里使用基于固定数量的邻居。
        NEIGHBORHOOD_NUM指定用户邻居数量
        */
        NearestNUserNeighborhood neighbor = new NearestNUserNeighborhood(NEIGHBORHOOD_NUM, similarity, dataModel);
        /*
        构建推荐器，协同过滤推荐有两种，分别是基于用户的和基于物品的，这里使用基于用户的协同过滤推荐
        构建基于用户的推荐系统
        */
        Recommender r = new GenericUserBasedRecommender(dataModel, neighbor, similarity);

        //得到所有用户的id集合
        LongPrimitiveIterator iter = dataModel.getUserIDs();
        List<Map> results = new ArrayList();
        while(iter.hasNext()) {
            Map u = new HashMap();
            String s = "";
            long uid = iter.nextLong();
            //获取推荐结果，获取指定用户指定数量的推荐结果
            List<RecommendedItem> list = r.recommend(uid, RECOMMENDER_NUM);
            u.put("用户", uid);
            System.out.printf("用户:%s", uid);
            //遍历推荐结果
            System.out.print("--》 推荐电影：");
            for(RecommendedItem ritem : list) {
                //获取推荐结果和推荐度
                System.out.print(ritem.getItemID()+"["+ritem.getValue()+"] ");
                s += (ritem.getItemID()+"["+ritem.getValue()+"] ");
            }
            u.put("推荐电影[相似度]", s);
            results.add(u);
            System.out.println();
        }
        return results;
    }

    public List forItem(Long userId, Long itemId, Integer howMany) throws TasteException, IOException {
//        JDBCDataModel dataModel = getJDBCDataModel();
        ClassPathResource classPathResource = new ClassPathResource("movie.data");
        DataModel dataModel  = new FileDataModel(classPathResource.getFile());

        /*
        计算相似度，相似度算法有很多种，欧几里得、皮尔逊等等。
        这里使用的是皮尔逊PearsonCorrelationSimilarity
        */
        ItemSimilarity itemSimilarity = new PearsonCorrelationSimilarity(dataModel);
        //构建推荐器，协同过滤推荐有两种，分别是基于用户的和基于物品的，这里使用基于物品的协同过滤推荐
        GenericItemBasedRecommender recommender = new GenericItemBasedRecommender(dataModel, itemSimilarity);
        //给指定用户推荐若干个与指定商品相似的商品
        List<RecommendedItem> recommendedItemList = recommender.recommendedBecause(userId, itemId, howMany);
        //打印推荐的结果
        System.out.println("使用基于物品的协同过滤算法");
        System.out.println(String.format("根据用户%s当前浏览的商品%s，推荐%s个相似的商品", userId, itemId, howMany));
        for (RecommendedItem recommendedItem : recommendedItemList) {
            System.out.println(recommendedItem);
        }
        return new ArrayList(){{
            for (RecommendedItem r : recommendedItemList) {
                add(new HashMap(){{
                    put("商品id", r.getItemID());
                    put("相似度", r.getValue());
                }});
            }
        }};
    }

    //  将数据加载到内存中，每一行是用户id、物品id、偏好。
    public JDBCDataModel getJDBCDataModel(){
        return new MySQLJDBCDataModel(DataSourceInfo.getDataSource(),"user_item","user_id","item_id","like_of",null);
    }

    public Double forEvaluate(Double evaluate) throws TasteException, IOException {
        //每次产生的随机数序列
        RandomUtils.useTestSeed();
        //将数据加载到内存中
//        DataModel dataModel = getJDBCDataModel2();
        ClassPathResource classPathResource = new ClassPathResource("ratings.data");
        DataModel dataModel  = new FileDataModel(classPathResource.getFile());

        //推荐评估，使用均方根
//        RecommenderEvaluator evaluator = new RMSRecommenderEvaluator();
        //这里使用的评估方法--平均差值
        RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();
        /*
        我们创建了一个推荐器生成器
        因为评估的时候我们需要将源数据中的一部分作为测试数据，其他作为算法的训练数据
        需要通过新训练的DataModel构建推荐器，所以采用生成器的方式生成推荐器
        */
        RecommenderBuilder builder = new RecommenderBuilder() {
            @Override
            public Recommender buildRecommender(DataModel dataModel) throws TasteException {
                UserSimilarity similarity = new PearsonCorrelationSimilarity(dataModel);
                UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, dataModel);
                return new GenericUserBasedRecommender(dataModel, neighborhood, similarity);
            }
        };
        System.out.println("evaluate " + evaluate);
        /*
        RecommenderEvaluator负责将数据分为训练集和测试集，用训练集构建一个DataModel和Recommender用来进行测试活动，得到结果之后在于真实数据进行比较。
        参数中0.7代表训练数据为70%，测试数据是30%。最后的1.0代表的是选取数据集的多少数据做整个评估。
        此处第二个null处，使用null就可以满足基本需求，但是如果我们有特殊需求，比如使用特殊的DataModel，在这里可以使用DataModelBuilder的一个实例。
        */
        double score = evaluator.evaluate(builder,null, dataModel, evaluate,1.0);
        /*
        最后得出的评估值越小，说明推荐结果越好
        最后的评价结果是0.782547494343349，表示的是平均与真实结果的差值是0.7.
        */
        System.out.println(score);
        return score;
    }

    public Map precisionAndRecall(Integer precisionAndRecallNum) throws TasteException, IOException {
        RandomUtils.useTestSeed();
        //将数据加载到内存中
//        DataModel dataModel = getJDBCDataModel2();
        ClassPathResource classPathResource = new ClassPathResource("ratings.data");
        DataModel dataModel  = new FileDataModel(classPathResource.getFile());

        RecommenderIRStatsEvaluator statsEvaluator = new GenericRecommenderIRStatsEvaluator();
        RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {
            @Override
            public Recommender buildRecommender(DataModel model) throws TasteException {
                UserSimilarity similarity = new PearsonCorrelationSimilarity(model);
                UserNeighborhood neighborhood = new NearestNUserNeighborhood(2, similarity, model);
                return new GenericUserBasedRecommender(model, neighborhood, similarity);
            }
        };
        /*
        计算推荐4个结果时的查准率和召回率，使用评估器，并设定评估期的参数
        4表示"precision and recall at 4"即相当于推荐top4，然后在top-4的推荐上计算准确率和召回率
        查准率为0.75 上面设置的参数为4，表示 Precision at 4（推荐4个结果时的查准率），平均有3/4的推荐结果是好的
        Recall at 4 推荐两个结果的查全率是1.0 表示所有的好的推荐都包含在这些推荐结果中
        */
        IRStatistics stats = statsEvaluator.evaluate(recommenderBuilder, null, dataModel, null,
                precisionAndRecallNum, GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0);
        System.out.println(stats.getPrecision());
        System.out.println(stats.getRecall());
       return new HashMap(){{
           put("准确率", stats.getPrecision());
           put("召回率", stats.getRecall());
       }};
    }

    //  将数据加载到内存中，每一行是用户id、物品id、偏好。
    public JDBCDataModel getJDBCDataModel2(){
        return new MySQLJDBCDataModel(DataSourceInfo.getDataSource(),"user_item2","user_id","item_id","like_of","create_time");
    }

}